假如想了解過程的讀者可以往下閱讀。
自己做一個集合動態生成HtmlTableHelper專案
一開始使用Reflection GetValue的方法,版友程凱大反映這樣效能不好,所以開始學習如何改為『緩存+動態生成Func換取速度』
,以下是學習過程
假設有一個類別,我想要動態取得它的屬性值
public class MyClass
{
public int MyProperty1 { get; set; }
public int MyProperty2 { get; set; }
}
一開始的直覺寫法,使用反射(Reflection.GetValue)
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };
var type = data.GetType();
var props = type.GetProperties();
var values = props.Select(s => s.GetValue(data)).ToList();
前者Reflection方式,可以發現每一次呼叫方法都要重新從Reflection GetProperties()
造成重複抓取資料動作
所以改建立一個System.Type擴充方法,從Cache取得屬性Info資料,並且使用ConcurrentDictionary達到線程安全。
public static partial class TypePropertyCacheHelper
{
private static readonly System.Collections.Concurrent.ConcurrentDictionary<RuntimeTypeHandle, IList<PropertyInfo>> TypeProperties
= new System.Collections.Concurrent.ConcurrentDictionary<RuntimeTypeHandle, IList<PropertyInfo>>();
public static IList<PropertyInfo> GetPropertiesFromCache(this Type type)
{
if (TypeProperties.TryGetValue(type.TypeHandle, out IList<PropertyInfo> pis))
return pis;
return TypeProperties[type.TypeHandle] = type.GetProperties().ToList();
}
}
使用方式:
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };
var type = data.GetType();
var props = type.GetPropertiesFromCache();
var values = props.Select(s => s.GetValue(data)).ToList();
主要邏輯
方法參數類別反推泛型
object GetterFunction(MyClass i) => i.MyProperty1 as object;
呼叫ToString避免boxing動作
public static partial class ValueGetter
{
private static readonly ConcurrentDictionary<string, object> Functions = new ConcurrentDictionary<string, object>();
public static object GetValueFromCache<T>(this PropertyInfo propertyInfo, T instance)
{
if (instance == null) throw new ArgumentNullException($"{nameof(instance)} is null");
if (propertyInfo == null) throw new ArgumentNullException($"{nameof(propertyInfo)} is null");
var type = propertyInfo.DeclaringType;
var key = $"{type.TypeHandle.Value.ToString()}|{propertyInfo.MetadataToken.ToString()}";
Func<T, object> function = null;
if (Functions.TryGetValue(key, out object func))
function = func as Func<T, object>;
else
{
function = CompileGetValueExpression<T>(propertyInfo);
Functions[key] = function;
}
return function(instance);
}
public static Func<T, object> CompileGetValueExpression<T>(PropertyInfo propertyInfo)
{
var instance = Expression.Parameter(propertyInfo.DeclaringType, "i");
var property = Expression.Property(instance, propertyInfo);
var convert = Expression.TypeAs(property, typeof(object));
var lambda = Expression.Lambda<Func<T, object>>(convert, instance);
return lambda.Compile();
}
}
但這時候會發現使用類型反推泛型
有一個問題,假如使用者傳入向上轉型
變數,代表CompileGetValueExpression<T>
方法的泛型T為object
這時候Compiler就會出現參數類型不符
錯誤,如圖片
object data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };
主要邏輯
function = func as Func<object, object>;
可以省掉轉型動作object GetterFunction(object i) => ((MyClass)i).MyProperty1 as object ;
強轉型動作
,但能避免參數類型不符問題(因為都是object型態)。public static partial class ValueGetter
{
private static readonly ConcurrentDictionary<object, Func<object, object>> Functions = new ConcurrentDictionary<object, Func<object, object>>();
public static object GetObject<T>(this PropertyInfo propertyInfo, T instance)
{
if (instance == null) return null;
if (propertyInfo == null) throw new ArgumentNullException($"{nameof(propertyInfo)} is null");
var key = $"{propertyInfo.DeclaringType.TypeHandle.Value.ToString()}|{propertyInfo.MetadataToken.ToString()}";
if (Functions.TryGetValue(key, out Func<object, object> function))
return function(instance);
return (Functions[key] = GetFunction(propertyInfo))(instance);
}
public static Func<object, object> GetFunction(PropertyInfo prop)
{
var type = prop.DeclaringType;
var propGetMethod = prop.GetGetMethod(nonPublic: true);
var propType = prop.PropertyType;
var dynamicMethod = new DynamicMethod("m", typeof(object), new Type[] { typeof(object) }, type.Module);
ILGenerator iLGenerator = dynamicMethod.GetILGenerator();
LocalBuilder local0 = iLGenerator.DeclareLocal(propType);
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Castclass, type);
iLGenerator.Emit(OpCodes.Call, propGetMethod);
iLGenerator.Emit(OpCodes.Box, propType);
iLGenerator.Emit(OpCodes.Ret);
return (Func<object, object>)dynamicMethod.CreateDelegate(typeof(Func<object, object>));
}
}
使用方式:
public static IEnumerable<object> GetValues<T>(T instance)
{
var type = instance.GetType();
var props = type.GetPropertiesFromCache();
return props.Select(s => s.GetObjectValue(instance));
}
圖片中CompilerFunction效率居然輸給Reflection GetValue
關鍵點在從Cache取值動作太繁雜
所以寫了最後一個版本,主要修改邏輯
藉由類別泛型來建立緩存字典
,Key取值不使用組合字串,直接使用int類型的PropertyInfo.MetadataToken
當緩存key值Boxing
動作using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ValueGetter
{
public static partial class ValueGetter
{
/// <summary>
/// Compiler Method Like:
/// <code>string GetterFunction(object i) => (i as MyClass).MyProperty1.ToString() ; </code>
/// </summary>
public static Dictionary<string, string> GetToStringValues<T>(this T instance)
=> instance?.GetType().GetPropertiesFromCache().ToDictionary(key => key.Name, value => value.GetToStringValue<T>(instance));
/// <summary>
/// Compiler Method Like:
/// <code>string GetterFunction(object i) => (i as MyClass).MyProperty1.ToString() ; </code>
/// </summary>
public static string GetToStringValue<T>(this PropertyInfo propertyInfo, T instance)
=> instance != null ? ValueGetterCache<T, string>.GetOrAddToStringFuntionCache(propertyInfo)(instance) : null;
}
public static partial class ValueGetter
{
/// <summary>
/// Compiler Method Like:
/// <code>object GetterFunction(object i) => (i as MyClass).MyProperty1 as object ; </code>
/// </summary>
public static Dictionary<string, object> GetObjectValues<T>(this T instance)
=> instance?.GetType().GetPropertiesFromCache().ToDictionary(key => key.Name, value => value.GetObjectValue(instance));
/// <summary>
/// Compiler Method Like:
/// <code>object GetterFunction(object i) => (i as MyClass).MyProperty1 as object ; </code>
/// </summary>
public static object GetObjectValue<T>(this PropertyInfo propertyInfo, T instance)
=> instance!=null?ValueGetterCache<T, object>.GetOrAddFunctionCache(propertyInfo)(instance):null;
}
internal partial class ValueGetterCache<TParam, TReturn>
{
private static readonly ConcurrentDictionary<int, Func<TParam, TReturn>> ToStringFunctions = new ConcurrentDictionary<int, Func<TParam, TReturn>>();
private static readonly ConcurrentDictionary<int, Func<TParam, TReturn>> Functions = new ConcurrentDictionary<int, Func<TParam, TReturn>>();
}
internal partial class ValueGetterCache<TParam, TReturn>
{
internal static Func<TParam, TReturn> GetOrAddFunctionCache(PropertyInfo propertyInfo)
{
var key = propertyInfo.MetadataToken;
if (Functions.TryGetValue(key, out Func<TParam, TReturn> func))
return func;
return (Functions[key] = GetCastObjectFunction(propertyInfo));
}
private static Func<TParam, TReturn> GetCastObjectFunction(PropertyInfo prop)
{
var instance = Expression.Parameter(typeof(TReturn), "i");
var convert = Expression.TypeAs(instance, prop.DeclaringType);
var property = Expression.Property(convert, prop);
var cast = Expression.TypeAs(property, typeof(TReturn));
var lambda = Expression.Lambda<Func<TParam, TReturn>>(cast, instance);
return lambda.Compile();
}
}
internal partial class ValueGetterCache<TParam, TReturn>
{
internal static Func<TParam, TReturn> GetOrAddToStringFuntionCache(PropertyInfo propertyInfo)
{
var key = propertyInfo.MetadataToken;
if (ToStringFunctions.TryGetValue(key, out Func<TParam, TReturn> func))
return func;
return (ToStringFunctions[key] = GetCastObjectAndToStringFunction(propertyInfo));
}
private static Func<TParam, TReturn> GetCastObjectAndToStringFunction(PropertyInfo prop)
{
var propType = prop.PropertyType;
var toStringMethod = propType.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(p => p.Name == "ToString").First();
var instance = Expression.Parameter(typeof(TParam), "i");
var convert = Expression.TypeAs(instance, prop.DeclaringType);
var property = Expression.Property(convert, prop);
var tostring = Expression.Call(property, toStringMethod);
var lambda = Expression.Lambda<Func<TParam, TReturn>>(tostring, instance);
return lambda.Compile();
}
}
public static partial class PropertyCacheHelper
{
private static readonly Dictionary<RuntimeTypeHandle, IList<PropertyInfo>> TypePropertiesCache = new Dictionary<RuntimeTypeHandle, IList<PropertyInfo>>();
public static IList<PropertyInfo> GetPropertiesFromCache(this Type type)
{
if (TypePropertiesCache.TryGetValue(type.TypeHandle, out IList<PropertyInfo> pis))
return pis;
return TypePropertiesCache[type.TypeHandle] = type.GetProperties().ToList();
}
}
}
使用方式:
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };
var result = data.GetObjectValues();
//Result:
Assert.AreEqual(123, result["MyProperty1"]);
Assert.AreEqual("test", result["MyProperty2"]);
邏輯:
public class BenchmarkBase
{
private static List<MyClass> Data = Enumerable.Range(1,100).Select(s=>new MyClass() { MyProperty1 = 123, MyProperty2 = "test" }).ToList();
[Benchmark()]
public void Reflection() => Data.Select(instance => {
var type = instance.GetType();
var props = type.GetProperties();
return props.ToDictionary(key => key.Name, value => value.GetValue(instance));
}).ToList();
[Benchmark()]
public void ReflectionToString() => Data.Select(instance => {
var type = instance.GetType();
var props = type.GetProperties();
return props.ToDictionary(key => key.Name, value => value.GetValue(instance).ToString());
}).ToList();
[Benchmark()]
public void GetObjectValues() => Data.Select(s => s.GetObjectValues()).ToList();
[Benchmark()]
public void GetObjectToStringValues() => Data.Select(s => s.GetToStringValues()).ToList();
}
結果:
BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.648 (1803/April2018Update/Redstone4)
Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0
ShortRun : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0
Method | Mean | Gen 0 | Allocated |
---|---|---|---|
GetObjectValues | 32.93 us | 9.8750 | 40.51 KB |
GetObjectToStringValues | 38.23 us | 10.0625 | 41.29 KB |
Reflection | 54.40 us | 10.0625 | 41.29 KB |
ReflectionToString | 60.24 us | 10.8125 | 44.42 KB |
這邊有做成一個NuGet套件ValueGetter,方便自己日後使用,也分享給有需要的版友
假如版友、前輩們有更好作法,期待留言、討論。
最後過程中在S.O尋求很多幫助,有興趣的讀者可以在我S.O查看歷史紀錄。
這篇講的很棒,超級詳細
自己有在專案中使用反射去產生 SQL 語法
不過沒有快取,剛好可以用這個套件優化
還有個問題想問
var key = $"{type.TypeHandle.Value.ToString()}|{propertyInfo.MetadataToken.ToString()}";
文中提到使用 .ToString()
可以避免 Boxing
是因為上面程式會被轉為
string.Concat(new object[] {type.TypeHandle.Value , propertyInfo.MetadataToken});
MetadataToken 因為是 int 所以會先 Boxing 嗎?
還是 TypeHandle.Value 的關係? (查了文件 TypeHandle.Value 型態是 IntPtr)
MetadataToken 因為是 int 所以會先 Boxing 嗎?
還是 TypeHandle.Value 的關係?
對,因為string.Concat需要接收參數為object,要對非字串變數做(object)boxing動作
public static string Concat(object arg0)
{
if (arg0 == null)
{
return Empty;
}
return arg0.ToString();
}
舉例:
var result = "123"+123;
編譯代碼是
string text = "123" + 123;
往下IL
IL_0000: nop
IL_0001: ldstr "123"
IL_0006: ldc.i4.s 7B
IL_0008: box System.Int32
IL_000D: call System.String.Concat
IL_0012: stloc.0 // result
IL_0013: ret
可以在IL_0003看到box動作
了解,直接看 IL 很清楚,感謝大大。